package org.ceteca.explica.client.installer;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.zip.ZipFile;

import org.ceteca.explica.core.InterfaceErrors;
import org.ceteca.explica.core.InterfaceExplica;
import org.ceteca.explica.core.installer.ResourceException;

public class ClientClassLoader extends URLClassLoader {
	public ClientClassLoader(ClassLoader parent) throws ResourceException {
		super(getClasspathUrls(InterfaceExplica.DEFAULT_CLASSPATH), parent);
		
		System.out.println("ClientClassLoader created: ");
		URL[] lUrls = this.getURLs();
		for (int i=0; i<lUrls.length; i++) {
			System.out.println(lUrls[i].getPath());
		}
	}
	
	public ClientClassLoader(String classpath, ClassLoader parent) throws ResourceException {
		super(getClasspathUrls(classpath), parent);
		
		System.out.println("ClientClassLoader created: ");
		URL[] lUrls = this.getURLs();
		for (int i=0; i<lUrls.length; i++) {
			System.out.println(lUrls[i].getPath());
		}
	}
	
	/**
	 * Constructor
	 * @param lUrls URL[], list of urls of the jars that must be 
	 * @param parent ClassLoader, parent ClassLoader in which all the 
	 * classes' resolutions are delegated.
	 */
	public ClientClassLoader(URL[] lUrls, ClassLoader parent){
		super(lUrls, parent);
		
		System.out.println("ClientClassLoader created: ");
		for (int i=0; i<lUrls.length; i++) {
			System.out.println(lUrls[i].getPath());
		}
	}
	
	/**
	 * Constructor
	 * @param lUrls URL[], list of urls of the jars that must be 
	 * @param parent ClassLoader, parent ClassLoader in which all the 
	 * classes' resolutions are delegated.
	 */
	public ClientClassLoader(URL[] lUrls){
		super(lUrls);
		
		System.out.println("ClientClassLoader created: ");
		for (int i=0; i<lUrls.length; i++) {
			System.out.println(lUrls[i].getPath());
		}
	}
	
	/**
	 * Find the specified class.
	 * @param name String, Class's path (excluding .class extension).
	 * @return Class, Class referenced by the classname.
	 * 
	 */
	protected Class findClass(String name) throws ClassNotFoundException {
		System.out.println("Finding the class: " + name);
		return super.findClass(name);
/*		
		byte[] classBytes = null;
		
		try	{
			System.out.println("Finding the class: " + name);
			
			// load the bytes of the referred class
			classBytes = loadClassBytes(name);
			
		}
		catch(IOException exception) {
			throw new ClassNotFoundException(name);
		}
		
		// Interpret the bytes loaded. The method defineClass must have a first 
		// null	parameter. Otherwise the system searchs for the class in the 
		// path the application is executed.
		Class cl = defineClass(null, classBytes, 0 ,classBytes.length);
		
		// If the class is null throw a ClassNotFoundException
		if(cl == null) {
			throw new ClassNotFoundException(name);
		}

		// Return the class loaded
		return cl;
*/
	}
	
	/**
	 * Load the bytes of the specified class.
	 * @param name String, classname excluding .class extension.
	 * @return byte[], bytecode of the class loaded.
	 * @throws IOException if any error takes place during the loading
	 * of the class's bytecode.
	 */
	private byte[] loadClassBytes(String name) throws IOException
	{
		// Add the .class extension to the classname
		name = name + ".class";
		
		// Open a FileInputStream to the class's file
		FileInputStream in = null;
		
		try {
			in = new FileInputStream(name);
			
			// Create an output buffer to store the bytecode (array of bytes)
			// of the class.
			ByteArrayOutputStream buffer = new ByteArrayOutputStream();
			int ch;

			// Read the file byte by byte
			while((ch = in.read()) != -1) {
				byte b = (byte)ch;
				
				// Write the byte read to the output buffer
				buffer.write(b);
			}
			
			// Return the bytes read (class's bytecode)
			return buffer.toByteArray();
		}
		// When the loading is finished, close the input stream
		finally	{
			if (in != null) 
				in.close();
		}
	}

	/**
	 * Build the list of the URLs referred by the classpath. It must include
	 * all the directories, all the files, and all the jars contained
	 * in the classpath.
	 * @param classpath String, classpath of the application. List of paths
	 * separated by the path.separator used by the system.
	 * @return URL[], list of individual URLs contained in the classpath.
	 * @throws ResourceException if any error take place during the building
	 * of the list of URLs enclosed by the classpath.
	 */
	public static final URL[] getClasspathUrls(String classpath) throws ResourceException {
		ArrayList lUrls = new ArrayList();
		
		// Convert classpath to a list of URLS
		if (classpath != null && classpath.length() > 0) {
			try {
				// Get the system's path separator
				String pathSep = System.getProperty(InterfaceExplica.PATH_SEPARATOR_PROPERTYNAME);
				
				if (pathSep == null || pathSep.length() <= 0) {
					throw new ResourceException(InterfaceErrors.ERR_RESOURCE_NULL_PATHSEPARATOR);
				}

				if (classpath == null || classpath.length() <= 0) {
					throw new ResourceException(InterfaceErrors.ERR_RESOURCE_NULL_CLASSPATH);
				}
				
				// Get a list with all the individual paths that form the classpath
				String[] lPaths = classpath.split(pathSep);
				if (lPaths == null || lPaths.length == 0) {
					throw new ResourceException(InterfaceErrors.ERR_RESOURCE_MALFORMED_CLASSPATH);
				}
				
				// Iterate each of the paths that form the classpath
				for (int i=0; i<lPaths.length; i++) {
					String path = lPaths[i];
					
					// if the path is a valid file add it to the list of URLs
					if (checkFile(path)) {
						lUrls.add(getPathURL(path));
						System.out.println("Add file to the resources urls: " + path);
					}
					// if the path is a valid directory
					else if (checkDirectory(path)) {
						// Add the directory to the list of URLs
						lUrls.add(getPathURL(removeLastSlash(path)));
						System.out.println("Add directory to the resources urls: " + removeLastSlash(path));
						
						// Add all the jars inside the directory to the list of URLs
						System.out.println("Adding jars of the directory to the resources urls: " + path);
						URL[] lJarUrls = getDirectoryJarsURL(removeLastSlash(path));
						if (lJarUrls != null && lJarUrls.length > 0)
							lUrls.addAll(Arrays.asList(lJarUrls));
					}
				}
			}
			catch (ResourceException ex) {
				ex.printStackTrace();
				throw ex;
			}
		} // if classpath not empty

		System.out.println("getClasspathUrls: ");
		for (int i=0; i<lUrls.size(); i++) {
			System.out.println(((URL)lUrls.get(i)).getPath());
		}
		
		return (lUrls != null && !lUrls.isEmpty())?(URL[])lUrls.toArray(new URL[lUrls.size()]):null;
	}
	
	/**
	 * Remove the last slash from the directory name. All the directories
	 * of the classpath must end without a slash.
	 * @param directory String, directory path to add to the classpath.
	 * @return String, correct and formatted directory path
	 * @throws ResourceException, if directory is null or empty. Also if the
	 * system's file separator is null or empty.
	 */
	private static final String removeLastSlash(String directory) throws ResourceException{
		String result = directory;
		String dirSeparator = null;

		// Get the system's file separator, stored in the system's property
		dirSeparator = System.getProperty(InterfaceExplica.FILE_SEPARATOR_PROPERTYNAME);

		// If directory is empty or null throw an exception
		if (directory == null || directory.length() <= 0) {
			throw new ResourceException(null, directory, InterfaceErrors.ERR_RESOURCE_INCORRECT_PATH);
		}
		
		// If system's file separator is empty or null throw an exception
		if (dirSeparator == null || dirSeparator.length() <= 0) {
			throw new ResourceException(null, directory, InterfaceErrors.ERR_RESOURCE_NULL_FILESEPARATOR);
		}
		
		// If directory ends with a file separator, remove the last file separator
		if(directory.endsWith(dirSeparator)) {
			result = directory.substring(0,directory.length() - dirSeparator.length());
		}		
		
		return result;
	}
	
	/**
	 * Checks if the path is a valid file (if it's really a single file
	 * and exists).
	 * @param path String, path of the file.
	 * @return boolean, True if the path is a valid file, False in any
	 * other case.
	 * @throws ResourceException when the path is malformed, null or empty.
	 */
	private static final boolean checkFile(String path) throws ResourceException{
		boolean result = false;
		
		if(path == null || path.length()<= 0) {
			throw new ResourceException(null, path, InterfaceErrors.ERR_RESOURCE_INCORRECT_PATH);
		}
		else {
			File f = null;
			try {
				f = new File(path);
				result = f.exists() && f.isFile();
			}
			catch (Exception e) {
				throw new ResourceException(null, path, InterfaceErrors.ERR_RESOURCE_INCORRECT_PATH, e);
			}
		}
		
		return result;
	}
	
	/**
	 * Checks if the path is a valid directory (if it's really a directory
	 * and exists).
	 * @param path String, path of the directory.
	 * @return boolean, True if the path is a valid directory, False in any
	 * other case.
	 * @throws ResourceException when the path is malformed, null or empty.
	 */
	private static final boolean checkDirectory(String path) throws ResourceException{
		boolean result = false;
		
		if(path == null || path.length()<= 0) {
			throw new ResourceException(null, path, InterfaceErrors.ERR_RESOURCE_INCORRECT_PATH);
		}
		else {
			File f = null;
			try {
				f = new File(path);
				result = f.exists() && f.isDirectory();
			}
			catch (Exception e) {
				System.err.println(e.getMessage());
				throw new ResourceException(null, path, InterfaceErrors.ERR_RESOURCE_INCORRECT_PATH, e);
			}
		}
		
		return result;
	}
	
	/**
	 * Checks if the path is a valid JAR file (if it's really a JAR file
	 * and exists).
	 * @param path String, path of the file.
	 * @return boolean, True if the path is a valid JAR file, False in any
	 * other case.
	 * @throws ResourceException when the path is malformed, null or empty.
	 */
	private static final boolean checkJarFile(String path) throws ResourceException{
		boolean result = false;

		System.out.println("Checking jar file: " + path);
		if(path == null || path.length()<= 0) {
			throw new ResourceException(null, path, InterfaceErrors.ERR_RESOURCE_INCORRECT_PATH);
		}
		else {
			ZipFile f = null;
			try {
				f = new ZipFile(path);
				result = f.size() > 0 && f.getName().toUpperCase().endsWith(".JAR");
			}
			catch (Exception e) {
				e.printStackTrace();
				result = false;
			}
		}
		
		return result;
	}
	
	/**
	 * Get the URL to the specified path (directory or single file).
	 * @param path String, path of the directory or file.
	 * @return URL, url to the specified path.
	 * @throws ResourceException, if the path is null, empty or malformed.
	 */
	private static final URL getPathURL(String path) throws ResourceException {
		if (path == null || path.length() <= 0) {
			throw new ResourceException(null, path, InterfaceErrors.ERR_RESOURCE_INCORRECT_PATH);
		}
		
		try {
			File f = new File(path);
			return f.toURI().toURL();
		}
		catch (Exception ex) {
			throw new ResourceException(null, path, InterfaceErrors.ERR_RESOURCE_INCORRECT_PATH, ex);
		}
	}
	
	/**
	 * Get the URLs of all the jar files contained in a directory path.
	 * @param path String, path of the directory.
	 * @return URL[], list with the URL of all the jar files contained 
	 * in the path.
	 * @throws ResourceException, if the path is null, empty or malformed.
	 */
	private static final URL[] getDirectoryJarsURL(String path) throws ResourceException {
		ArrayList lUrls = new ArrayList();
		
		if (path == null || path.length() <= 0) {
			throw new ResourceException(null, path, InterfaceErrors.ERR_RESOURCE_INCORRECT_PATH);
		}
		
		try {
			// Get the system's file separator, stored in the system's property
			String dirSeparator = System.getProperty(InterfaceExplica.FILE_SEPARATOR_PROPERTYNAME); 
			
			// Open the directory file
			File f = new File(path);
			
			System.out.println("Directory opened!");
			
			// Check the directory is a valid directory
			if (f.exists() && f.isDirectory()) {
				// Get all the files belonging to the directory
				String[] lFiles = f.list();
				System.out.println("Files in the directory: " + lFiles);
				
				// for each file/subdirectory in the directory
				for (int i=0; lFiles != null && i<lFiles.length; i++) {
					String filepath = path + dirSeparator + lFiles[i];
					System.out.println("Analizing directory's file: " + filepath);
					
					// if the file is a JAR file then add it to the list of URLs
					if (checkJarFile(filepath)) {
						lUrls.add(getPathURL(filepath));
						System.out.println("Add jar to the resources urls: " + filepath);
					}
					// if the file is a directory then navigate the subdirectory to add the jar
					// files that may be stored in the subdirectory and its succesors.
					else if (checkDirectory(filepath)) {
						lUrls.addAll(Arrays.asList(getDirectoryJarsURL(filepath)));
					}
				} // for
			} // if path is a valid directory
			else {
				throw new ResourceException(null, path, InterfaceErrors.ERR_RESOURCE_INCORRECT_DIRECTORY);
			} // else path is not a valid directory
			
			
			System.out.println("getDirectoryJarsUrls: ");
			for (int i=0; i<lUrls.size(); i++) {
				System.out.println(((URL)lUrls.get(i)).getPath());
			}
			return (lUrls != null && !lUrls.isEmpty())?(URL[])lUrls.toArray(new URL[lUrls.size()]):null;
		}
		catch (Exception ex) {
			ex.printStackTrace();
			throw new ResourceException(null, path, InterfaceErrors.ERR_RESOURCE_INCORRECT_PATH, ex);
		}
	}

	public Class loadClass(String name) throws ClassNotFoundException {
		System.out.println("Llamando al loadClass del ClientClassLoader: " + name);
		URL[] lUrls = this.getURLs();
		for (int i=0; i<lUrls.length; i++) {
			System.out.println(lUrls[i].getPath());
		}
		return super.loadClass(name);
	}
}
